package br.mikhas.util;

import static br.mikhas.util.Assert.notEmpty;
import static br.mikhas.util.Assert.notNull;
import static br.mikhas.util.TypeUtils.getWrapper;
import static br.mikhas.util.TypeUtils.isPrimitive;
import static br.mikhas.util.TypeUtils.isWrapper;

import java.lang.reflect.Constructor;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.HashSet;
import java.util.Set;

/**
 * Provide some methods to help working with numbers
 * 
 * @author Mikhail Domanoski
 */

public class Numbers {

	private Numbers() {
	}

	// Holds all default number classes
	private static final Set<Class<? extends Number>> NUMBERS;

	static {

		NUMBERS = new HashSet<Class<? extends Number>>(14);
		NUMBERS.add(short.class);
		NUMBERS.add(byte.class);
		NUMBERS.add(int.class);
		NUMBERS.add(long.class);
		NUMBERS.add(float.class);
		NUMBERS.add(double.class);
		NUMBERS.add(Short.class);
		NUMBERS.add(Integer.class);
		NUMBERS.add(Long.class);
		NUMBERS.add(Float.class);
		NUMBERS.add(Double.class);
		NUMBERS.add(Byte.class);
		NUMBERS.add(BigInteger.class);
		NUMBERS.add(BigDecimal.class);
	}

	/**
	 * Checks if the object is an integer number
	 * 
	 * @param obj
	 *            The possible integer class
	 * @return <code>true</code> if it's a integer
	 */
	public static boolean isInteger(Object object) {
		return object == null ? false : object.getClass() == Integer.class;
	}

	/**
	 * Checks if the object is a float number
	 * 
	 * @param obj
	 *            The possible float class
	 * @return <code>true</code> if it's a float
	 */
	public static boolean isFloat(Object object) {
		return object == null ? false : object.getClass() == Float.class;
	}

	/**
	 * Checks if the object is a long number
	 * 
	 * @param obj
	 *            The possible long class
	 * @return <code>true</code> if it's a long
	 */
	public static boolean isLong(Object object) {
		return object == null ? false : object.getClass() == Long.class;
	}

	/**
	 * Checks if the object is a short number
	 * 
	 * @param obj
	 *            The possible short class
	 * @return <code>true</code> if it's a short
	 */
	public static boolean isShort(Object object) {
		return object == null ? false : object.getClass() == Short.class;
	}

	/**
	 * Checks if the object is a double number
	 * 
	 * @param obj
	 *            The possible double class
	 * @return <code>true</code> if it's a double
	 */
	public static boolean isDouble(Object object) {
		return object == null ? false : object.getClass() == Double.class;
	}

	/**
	 * Checks if the class is a class number
	 * 
	 * @param cls
	 *            The possible number class
	 * @return <code>true</code> if it's a number
	 */
	public static boolean isNumber(Class<?> cls) {
		return cls == null ? false : NUMBERS.contains(cls);
	}

	/**
	 * Checks if the object is a number
	 * 
	 * @param obj
	 *            The possible number object
	 * @return <code>true</code> if it's a number
	 */
	public static boolean isNumber(Object obj) {
		return obj == null ? false : NUMBERS.contains(obj.getClass());
	}

	/**
	 * Creates a numebr from a String according to the desired number class
	 * 
	 * @param clazz
	 *            The number class
	 * @param number
	 *            The string representation of the number
	 * @return and instance of the desired number
	 */
	@SuppressWarnings("unchecked")
	public static <T extends Number> T getNumber(Class<T> clazz, String number) {
		notNull(clazz);
		notEmpty(number);

		if (!isNumber(clazz))
			throw new IllegalArgumentException(
					"The providen class is not a number: " + clazz);

		Class<? extends Number> cls = null;
		if (isWrapper(clazz)) {
			cls = (Class<? extends Number>) clazz;
		} else if (isPrimitive(clazz)) {
			cls = (Class<? extends Number>) getWrapper(clazz);
		}
		return (T) instantiateNumber(cls, number);
	}

	/**
	 * Creates a numebr from an Object according to the desired number class
	 * 
	 * @param clazz
	 *            The number class
	 * @param number
	 *            The string representation of the number
	 * @return and instance of the desired number
	 */
	@SuppressWarnings("unchecked")
	public static <T extends Number> T getNumber(Class<T> clazz, Object number) {
		notNull(clazz);

		if (number instanceof Number) {
			Number num = (Number) number;

			if (num.getClass() == clazz)
				return (T) num;

			if (clazz == Integer.class || clazz == int.class)
				return (T) Integer.valueOf(num.intValue());

			if (clazz == Short.class || clazz == short.class)
				return (T) Short.valueOf(num.shortValue());

			if (clazz == Long.class || clazz == long.class)
				return (T) Long.valueOf(num.longValue());

			if (clazz == Double.class || clazz == double.class)
				return (T) Double.valueOf(num.doubleValue());

			if (clazz == Float.class || clazz == float.class)
				return (T) Float.valueOf(num.floatValue());

			if (clazz == BigInteger.class)
				return (T) BigInteger.valueOf(num.longValue());

			if (clazz == BigDecimal.class)
				return (T) BigDecimal.valueOf(num.doubleValue());
		}
		return getNumber(clazz, String.valueOf(number));
	}

	/**
	 * Gets the lowest numbers in the array.
	 * 
	 * @param numbers
	 *            An array of numbers to look for the lowest one
	 * @return The lowest number
	 */
	@SuppressWarnings("unchecked")
	public static <T extends Number> T min(T... numbers) {
		notEmpty(numbers);

		Class<? extends Number> arrayType = (Class<? extends Number>) numbers
				.getClass().getComponentType();

		T min = numbers[0];

		for (int i = 1; i < numbers.length; i++)
			if (compare(arrayType, numbers[i], min) < 0)
				min = numbers[i];

		return min;
	}

	/**
	 * Gets the lowest int in the array
	 * 
	 * @param numbers
	 *            An array of int numbers
	 * @return the lowest int value in the array
	 */
	public static int min(int[] numbers) {
		notEmpty(numbers);
		int min = numbers[0];

		for (int i = 1; i < numbers.length; i++)
			if (numbers[i] < min)
				min = numbers[i];

		return min;
	}

	/**
	 * Gets the lowest long in the array
	 * 
	 * @param numbers
	 *            An array of long numbers
	 * @return the lowest long value in the array
	 */
	public static long min(long[] numbers) {
		notEmpty(numbers);
		long min = numbers[0];

		for (int i = 1; i < numbers.length; i++)
			if (numbers[i] < min)
				min = numbers[i];

		return min;
	}

	/**
	 * Gets the lowest double in the array
	 * 
	 * @param numbers
	 *            An array of double numbers
	 * @return the lowest double value in the array
	 */
	public static double min(double[] numbers) {
		notEmpty(numbers);
		double min = numbers[0];

		for (int i = 1; i < numbers.length; i++)
			if (numbers[i] < min)
				min = numbers[i];

		return min;
	}

	/**
	 * Internally compare numbers
	 * 
	 * @param <T>
	 *            A number type
	 * @param a
	 *            The first number to compare
	 * @param b
	 *            The second number to compare
	 * @return The comparation of the numbers
	 */
	private static <T extends Number> int compare(Class<?> type, T a, T b) {
		if (type == Integer.class || type == int.class) {
			int A = a.intValue(), B = b.intValue();
			return A > B ? 1 : A < B ? -1 : 0;
		} else if (type == Long.class || type == long.class) {
			long A = a.longValue(), B = b.longValue();
			return A > B ? 1 : A < B ? -1 : 0;
		} else if (type == Double.class || type == double.class) {
			double A = a.doubleValue(), B = b.doubleValue();
			return A > B ? 1 : A < B ? -1 : 0;
		} else if (type == Float.class || type == float.class) {
			float A = a.floatValue(), B = b.floatValue();
			return A > B ? 1 : A < B ? -1 : 0;
		} else if (type == Short.class || type == short.class) {
			short A = a.shortValue(), B = b.shortValue();
			return A > B ? 1 : A < B ? -1 : 0;
		} else if (type == BigDecimal.class) {
			return ((BigDecimal) a).compareTo((BigDecimal) b);
		} else if (type == BigInteger.class) {
			return ((BigInteger) a).compareTo((BigInteger) b);
		} else {
			long A = a.longValue(), B = b.longValue();
			return A > B ? 1 : A < B ? -1 : 0;
		}
	}

	/**
	 * Creates a new instance of the number
	 * 
	 * @param cls
	 *            The number class
	 * @param number
	 *            The string representation of the desired number
	 * @return The instance of the number
	 */
	@SuppressWarnings("unchecked")
	private static Number instantiateNumber(Class<? extends Number> cls,
			String number) {
		assert cls != null : "The class may not be null";

		if (number == null || number.isEmpty())
			return null;

		try {
			Constructor<Number> numberConstructor = (Constructor<Number>) cls
					.getConstructor(String.class);
			Number resnum = numberConstructor.newInstance(number);
			return resnum;
		} catch (Exception t) {
			throw ExceptionUtils.wrapChecked(t);
		}
	}
}
